<?php
/**
 * 文件服务器类库
 */

namespace app\common\lib;

use app\common\lib\Redis;
use app\common\lib\Image;
use think\facade\Validate;

class File
{
    private $dir = '';
    private $appKey = '';
    private $appDir = '';
    private static $domain = '';
    private $saveDir = '';
    /**
     * 上传的文件类型
     */
    const FILE_TYPE_TMP = 1; //通过$_FILES上传
    const FILE_TYPE_STREAM = 2; //通过流类型

    const MAX_BLOCK_NUM = 220; //最多分块数
    const BLOCK_SIZE = 10240; //单个文件限制大小M

    function __construct($appKey)
    {
        $this->appKey = $appKey;
        $baseDir = public_path() . '/upload/';
        $this->dir = $baseDir . $this->appKey;
        $this->appDir = $this->appKey;
        self::$domain = config('app.app_host');
        self::$domain = trim(self::$domain, '/');
        self::$domain = self::$domain . '/';
        $this->saveDir = 'upload/' . $this->appDir;
    }

    /**
     * 上传
     * User:端木凌风
     * Date:2023-11-08 09:25
     * @param array $param
     * @return object|array|void|mixed
     */
    public function upload(array $param = [])
    {

        if (!$this->initDir()) {
            return funReturn(STATUS_CODE_DATAERROR, '上传失败');
        }
        $checkRes = $this->checkData($param);
        if ($checkRes['status'] != STATUS_CODE_SUCCESS) {
            return $checkRes;
        }
        $pathRes = $this->createPath($checkRes['data']['path']);
        if ($pathRes['status'] != STATUS_CODE_SUCCESS) {//创建文件目录
            return $pathRes;
        }
        $resizeData = $checkRes['data']['resize_data'];
        $dataPath = implode('/', $checkRes['data']['path']) . '/';
        $path = $this->dir . '/' . $dataPath;
        $isBlock = $param['block_total'] > 1 ? true : false;
        $blockFileName = $isBlock ? md5($param['file_md5'] . $param['new_filename']) : '';
        $blockName = $blockFileName . '_' . $param['block_sn'];
        $fileName = $isBlock ? $blockName : $param['new_filename'];
        /*上传文件*/
        $uploadRes = $this->moveUpload($path, $fileName, $param['file'], $param['type'], $isBlock);
        if ($uploadRes['status'] != STATUS_CODE_SUCCESS) {
            return funReturn(STATUS_CODE_INSIDEERROR, '上传失败');
        }
        if ($isBlock) {
            Redis::set($this->getFileBlockCacheKey($blockName, $path), 1, 'EX', 3600 * 4);
        }
        $isFinish = $isBlock ? 0 : 1;
        if ($isBlock == true and $this->checkAllBlockIsUpload($path, $param['block_total'], $blockFileName)) {
            $mergeRes = $this->mergeBlock($path, $param['block_total'], $param['new_filename'], $param['file_md5'], $param['type']);
            if ($mergeRes['status'] != STATUS_CODE_SUCCESS) {
                return $mergeRes;
            }
            if ($mergeRes['status'] == STATUS_CODE_SUCCESS) {
                $fileName = $param['new_filename'];
                $this->deleteFile($mergeRes['data']['block']);
                $isFinish = 1;
            }
        }
        $originFileName = $fileName;
        if ($isFinish) {
            if ($param['block_total'] > 1) { //检查文件MD5值 防止文件被篡改
                if ($param['file_md5'] != md5_file($path . $fileName)) {
                    /*无效文件 需要删除*/
                    unlink($path . $fileName);
                    return funReturn(STATUS_CODE_DATAERROR, '文件被篡改');
                }
            }
            if ($resizeData) {
                if (!$this->checkIsImag($fileName)) {
                    return funReturn(STATUS_CODE_DATAERROR, '不是图片');
                }
                $resizeRes = $this->resizeImg($path . $fileName, $resizeData['width'], $resizeData['height'], $fileName, $path, $resizeData['left_original'], $resizeData['resize_type'], $resizeData['bgcolor']);
                if ($resizeRes['status'] != STATUS_CODE_SUCCESS) {
                    return $resizeRes;
                }
                $fileName = $resizeRes['data'];
            }
        }
        $saveFile = self::$domain . 'upload/' . $this->appDir . '/' . $dataPath . $fileName;
        $originFile = self::$domain . 'upload/' . $this->appDir . '/' . $dataPath . $originFileName;
        $mqObj = new \App\Common\Lib\MsgQueue();
        $mqObj->sendMessage(MsgQueue::QUEUE_FILE_UPLOAD,
            [
                'save_file' => $this->appDir . '/' . $dataPath . $fileName,
                'origin_file' => $this->appDir . '/' . $dataPath . $originFileName,
                'api_key' => $this->appKey,
                'file_time' => date('Y-m-d H:i:s')
            ], false);
        return funReturn(STATUS_CODE_SUCCESS, '上传成功', ['path' => $saveFile, 'origin' => $originFile, 'isFinish' => $isFinish]);
    }

    /**
     * 获取分块缓存key
     * User:端木凌风
     * Date:2023-11-08 09:37
     * @param $blockName
     * @param $path
     * @return object|array|void|mixed
     */
    private function getFileBlockCacheKey($blockName, $path)
    {
        $path = trim($path, '/');
        $key = 'FILES:TEMP:' . md5($path) . ':' . $blockName;
        return $key;
    }

    /**
     * 调整图片尺寸
     * @param $file
     * @param $width
     * @param $height
     * @param $fileName
     * @param $path
     * @param $isLeftoriginal
     * @param int $scaleType
     * @param string $bgColor
     */
    private function resizeImg($file, $width, $height, $fileName, $path, $isLeftoriginal, $scaleType = 1, $bgColor = "#FFFFFF")
    {
        try {
            $imgOrg = Image::make($file);
            //原图宽度和高度
            $w = $imgOrg->width();
            $h = $imgOrg->height();
            if ($w < $width && $h < $height) {
                $scale = 1;
            } else {
                $scale = min($width / $w, $height / $h);
            }
            //设置缩略图的坐标及宽度和高度
            $newW = $w * $scale;
            $newH = $h * $scale;
            $saveFile = $fileName;
            if ($isLeftoriginal) {
                $fileArr = explode('.', $file);
                $fileType = end($fileArr);
                $fileType = strtolower($fileType);
                $saveFile = $this->createFileName($path . $fileName) . '.' . $fileType;
            }
            $fullFilePath = $path . $saveFile;
            switch ($scaleType) {
                case 1: //等比例缩放
                    $imgOrg->resize($newW, $newH);
                    $imgOrg->save($fullFilePath);
                    $imgOrg->destroy();
                    break;
                case 2: //填充
                    $imgOrg->resize($newW, $newH);
                    $posx = intval(($width - $w * $scale) / 2);
                    $posy = intval(($height - $h * $scale) / 2);
                    $bgColor = ($bgColor == '') ? null : $bgColor;
                    if ($bgColor) {
                        $imgThumb = Image::canvas($width, $height, $bgColor);
                    } else {
                        $imgThumb = Image::canvas($width, $height);
                    }
                    $imgThumb->insert($imgOrg, 'top-left', $posx, $posy);
                    $imgThumb->save($fullFilePath);
                    $imgThumb->destroy();
                    $imgOrg->destroy();
                    break;
            }
            return funReturn(STATUS_CODE_SUCCESS, 'success', $saveFile);
        } catch (\Exception $e) {
            return funReturn(STATUS_CODE_INSIDEERROR, '生成缩略图失败');
        }
    }

    /**
     * 验证是否为图片
     * User:端木凌风
     * Date:2023-11-08 09:16
     * @param $file
     * @return object|array|void|mixed
     */
    private function checkIsImag($file)
    {
        if (!$file) {
            return false;
        }
        $fileArr = explode('.', $file);
        $fileType = end($fileArr);
        $fileType = strtolower($fileType);
        if (in_array($fileType, ['bmp', 'jpg', 'jpeg', 'gif', 'png'])) {
            return true;
        }
        return false;
    }


    /**
     * 存储路径转化为url
     * User:端木凌风
     * Date:2023-11-08 09:28
     * @param $filePath
     * @return object|array|void|mixed
     */
    public function formatFilePath($filePath)
    {
        return [
            'path' => 'upload/' . $this->appDir . '/' . $filePath,
            'url' => self::$domain . 'upload/' . $this->appDir . '/' . $filePath
        ];
    }

    /**
     * 删除文件
     * User:端木凌风
     * Date:2023-11-08 09:43
     * @param array $file
     * @return object|array|void|mixed
     */
    private function deleteFile(array $file = [])
    {
        foreach ($file as $v) {
            if (file_exists($v)) {
                unlink($v);
            }
        }
        return true;
    }

    /**
     * 移动文件
     * User:端木凌风
     * Date:2023-11-08 09:07
     * @param $path
     * @param $fileName
     * @param $file
     * @param $type
     * @param $isBlock
     * @return object|array|void|mixed
     */
    public function moveUpload($path, $fileName, $file, $type, $isBlock)
    {
        if ($type == self::FILE_TYPE_TMP) {
            if ($file->getSize() / 1024 > self::BLOCK_SIZE) {
                return funReturn(STATUS_CODE_DATAERROR, '文件尺寸不能大于' . self::BLOCK_SIZE . 'Kb');
            }
            if ($file->move($path, $fileName)) {
                return funReturn(STATUS_CODE_SUCCESS, 'success');
            }
        } else if ($type == self::FILE_TYPE_STREAM) {
            if (strlen($file) / 1024 > self::BLOCK_SIZE) {
                return funReturn(STATUS_CODE_DATAERROR, '文件尺寸不能大于' . self::BLOCK_SIZE . 'Kb');
            }
            /*$fp = fopen($path.$fileName,'w+');
            $res = fwrite($fp,base64_decode($file));
            fclose($fp);
            if($res){
                return funReturn(STATUS_CODE_SUCCESS, 'success');
            }*/
            if ($isBlock) {
                $res = file_put_contents($path . $fileName, $file);
            } else {
                $res = file_put_contents($path . $fileName, base64_decode($file));
            }
            $file = null;
            unset($file);
            if ($res !== false) {
                return funReturn(STATUS_CODE_SUCCESS, 'success');
            }
        }
        return funReturn(STATUS_CODE_DATAERROR, '上传失败');
    }

    /**
     * 检查所有的分块是否上传
     * User:端木凌风
     * Date:2023-11-08 09:32
     * @param $path
     * @param $totalBlock
     * @param $fileName
     * @return object|array|void|mixed
     */
    private function checkAllBlockIsUpload($path, $totalBlock, $fileName)
    {
        for ($i = 1; $i <= $totalBlock; $i++) {
            /*$blockFileName = $path. $fileName.'_'.$i;
            if(!file_exists($blockFileName)) {
               return false;
            }*/
            $blockFileKey = $this->getFileBlockCacheKey($fileName . '_' . $i, $path);
            if (!Redis::get($blockFileKey)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 分块合并
     * User:端木凌风
     * Date:2023-11-08 09:43
     * @param $path
     * @param $totalBlock
     * @param $fileName
     * @param $fileMd5
     * @param $type
     * @return object|array|void|mixed
     */
    private function mergeBlock($path, $totalBlock, $fileName, $fileMd5, $type)
    {
        $blockFile = [];
        $bolkNamePreFix = $path . md5($fileMd5 . $fileName);
        if (file_exists($path . $fileName)) {
            return funReturn(STATUS_CODE_DATAERROR, '已经存在');
        }
        $fp = fopen($path . $fileName, "a+");
        if (!$fp) {
            return funReturn(STATUS_CODE_DATAERROR, '已经存在');
        }
        flock($fp, LOCK_EX);
        $isFail = false;
        for ($i = 1; $i <= $totalBlock; $i++) {
            $blockFileName = $bolkNamePreFix . '_' . $i;
            /*if(!file_exists($blockFileName)) {
                return funReturn(STATUS_CODE_DATAERROR, '文件丢失'.$blockFileName);
            }*/
            $blockFile[] = $blockFileName;
            if ($type == self::FILE_TYPE_STREAM) {
                if (!fwrite($fp, base64_decode(file_get_contents($blockFileName)))) {
                    $isFail = true;
                }
            } else {
                if (!fwrite($fp, file_get_contents($blockFileName))) {
                    $isFail = true;
                }
            }
        }
        flock($fp, LOCK_UN);
        fclose($fp);
        if (!$isFail) {
            return funReturn(STATUS_CODE_SUCCESS, '', ['block' => $blockFile]);
        }
        return funReturn(STATUS_CODE_INSIDEERROR, '上传失败');
    }

    /**
     * 初始化根目录
     * User:端木凌风
     * Date:2023-11-08 09:14
     * @return object|array|void|mixed
     */
    private function initDir()
    {
        if (file_exists($this->dir)) {
            return true;
        }
        mkdir($this->dir);
        return true;
    }

    /**
     * 创建文件路径
     * User:端木凌风
     * Date:2023-11-08 09:27
     * @param $path
     * @return object|array|void|mixed
     */
    public function createSavePath($path)
    {
        if (!$this->initDir()) {
            return funReturn(STATUS_CODE_DATAERROR, '上传失败');
        }
        $path = trim($path, '/');
        $pathArr = explode('/', $path);
        $res = $this->createPath($pathArr);
        if ($res['status'] != STATUS_CODE_SUCCESS) {
            return $res;
        }
        return funReturn(STATUS_CODE_SUCCESS, 'success', $this->dir . '/' . $path);
    }


    /**
     * 创建目录
     * User:端木凌风
     * Date:2023-11-08 09:39
     * @param array $path
     * @return object|array|void|mixed
     */
    private function createPath(array $path = [])
    {
        $dir = $this->dir;
        foreach ($path as $v) {
            if (!preg_match("/^[A-Za-z0-9_]+$/", $v)) {
                return funReturn(STATUS_CODE_DATAERROR, 'path只能由英文字母、数字、下划线组成');
            }
        }
        foreach ($path as $v) {
            $dir = $dir . '/' . $v;
            if (file_exists($dir)) {
                continue;
            }
            mkdir($dir);
        }
        return funReturn(STATUS_CODE_SUCCESS, 'success');
    }

    /**
     * 检验数据
     * User:端木凌风
     * Date:2023-11-08 09:49
     * @param $param
     * @return object|array|void|mixed
     */
    private function checkData(array $param = [])
    {
        $rule = [
            'path' => 'require|length:0,60',
            'block_sn' => 'require|integer|min:1',
            'block_total' => 'require|integer|min:1',
            'new_filename' => 'require|length:0,120',
            'file' => "require",
            'type' => 'require|in:1,2',
        ];
        $message = [
            'path.require' => '缺少path参数',
            'path.length' => 'path字段不能超过60这个字符',
            'block_sn.require' => '缺少block_sn参数',
            'block_sn.integer' => 'block_sn参数必须是整型数字',
            'block_sn.min' => 'block_sn参数最小是1',
            'block_total.require' => '缺少block_total参数',
            'block_total.integer' => 'block_total参数必须是整型数字',
            'block_total.min' => 'block_total参数最小是1',
            'new_filename.require' => '缺少new_filename参数',
            'new_filename.length' => 'new_filename字段不能超过120个字符',
            'file.require' => '缺少file参数',
            'type.require' => '缺少type参数',
            'type.in' => 'type参数只能是1或2',
        ];
        $validate = Validate::message($message)->rule($rule);
        if (!$validate->check($param)) {
            return funReturn(STATUS_CODE_DATAERROR, $validate->getError());
        }


        if (!preg_match("/(^[A-Za-z0-9.-_]+$)+/", $param['new_filename'])) {
            return funReturn(STATUS_CODE_DATAERROR, 'new_filename字段只能由英文字母、数字、下划线组成');
        }

        if ($param['block_sn'] > $param['block_total']) {
            return funReturn(STATUS_CODE_DATAERROR, 'block_sn 不能大于 block_total');
        }
        if ($param['block_total'] > self::MAX_BLOCK_NUM) {
            return funReturn(STATUS_CODE_DATAERROR, 'block_total 不能大于' . self::MAX_BLOCK_NUM);
        }
        if ($param['block_total'] > 1) {
            if (!isset($param['file_md5'])) {
                return funReturn(STATUS_CODE_DATAERROR, '缺少file_md5参数');
            }
        }
        $param['path'] = trim($param['path'], '/');
        $pathArr = explode('/', $param['path']);
        if (isset($param['request_type']) and $param['request_type'] == 'rpc') {
            if ($param['type'] != 2) {
                return funReturn(STATUS_CODE_DATAERROR, 'type参数错误 只能为2');
            }
            if (!is_string($param['file'])) {
                return funReturn(STATUS_CODE_DATAERROR, '只能为文件流');
            }
        }
        $data = [
            'block_sn' => $param['block_sn'],
            'block_total' => $param['block_total'],
            'path' => $pathArr,
            'file' => ($param['type'] == 1) ? request()->file('file') : $param['file'],
            'new_filename' => $param['new_filename'],
            'resize_data' => []
        ];
        if (isset($param['resize'])) {
            $param['resize'] = json_decode($param['resize'], true);
            if (!is_array($param['resize'])) {
                return funReturn(STATUS_CODE_DATAERROR, 'resize参数错误');
            }
            if (!$this->checkIsImag($param['new_filename'])) {
                return funReturn(STATUS_CODE_DATAERROR, '不是图片');
            }
            $resizeRes = $this->checkReSizeParam($param['resize']);
            if ($resizeRes['status'] != STATUS_CODE_SUCCESS) {
                return $resizeRes;
            }
            $data['resize_data'] = $resizeRes['data'];
        }
        return funReturn(STATUS_CODE_SUCCESS, '验证成功', $data);
    }

    /**
     * 验证参数
     * User:端木凌风
     * Date:2023-11-08 09:31
     * @param $param
     * @return object|array|void|mixed
     */
    private function checkReSizeParam($param)
    {

        $rule = [
            'width' => 'require|integer|min:1',
            'height' => 'require|integer|min:1',
            'resize_type' => 'require|integer|in:1,2',
            'left_original' => "in:1,2",
        ];
        $message = [
            'width.require' => '缺少width参数',
            'width.integer' => 'width必须为整数',
            'width.min' => 'width不能小于1',
            'height.require' => '缺少height参数',
            'height.integer' => 'height必须为整数',
            'height.min' => 'height不能小于1',
            'resize_type.require' => '缺少resize_type参数',
            'resize_type.integer' => 'resize_type必须为整数',
            'resize_type.in' => 'resize_type错误',
            'left_original.in' => 'left_original错误',
        ];
        $validate = Validate::message($message)->rule($rule);
        if (!$validate->check($param)) {
            return funReturn(STATUS_CODE_DATAERROR, $validate->getError());
        }

        $data = [
            'width' => $param['width'],
            'height' => $param['height'],
            'resize_type' => $param['resize_type'],
            'left_original' => $param['left_original'] ?? 0,
            'bgcolor' => '#FFFFFF'
        ];
        if (key_exists('bgcolor', $param)) {
            $data['bgcolor'] = $param['bgcolor'];
            if ($data['bgcolor'] !== '') {
                if (!$data['bgcolor']) {
                    return funReturn(STATUS_CODE_DATAERROR, 'bgcolor参数非法');
                }
                if ($data['bgcolor'][0] != '#') {
                    return funReturn(STATUS_CODE_DATAERROR, 'bgcolor参数非法');
                }
                if (strlen($data['bgcolor']) != 7) {
                    return funReturn(STATUS_CODE_DATAERROR, 'bgcolor参数非法');
                }
            }
        }
        return funReturn(STATUS_CODE_SUCCESS, '验证成功', $data);
    }

    /**
     * 把字节流保存成图片
     * User:端木凌风
     * Date:2023-11-08 09:51
     * @param $savePath
     * @param $extions
     * @param $stream
     * @return object|array|void|mixed
     */
    public function saveStreamToFile($savePath, $extions, $stream)
    {
        $savePath = trim($savePath, '/');
        $pathRes = $this->createSavePath($savePath);
        if ($pathRes['status'] != STATUS_CODE_SUCCESS) {
            return funReturn(STATUS_CODE_INSIDEERROR, '创建失败');
        }
        $fileName = $this->createFileName($pathRes['data']) . '.' . $extions;
        if (file_put_contents($pathRes['data'] . '/' . $fileName, $stream) === false) {
            return funReturn(STATUS_CODE_INSIDEERROR, '创建失败');
        }
        return funReturn(STATUS_CODE_SUCCESS, 'success', ['file' => $this->saveDir . '/' . $savePath . '/' . $fileName]);
    }

    /**
     * 下载远程文件到本地
     * User:端木凌风
     * Date:2023-11-08 09:16
     * @param $url
     * @param $savePath
     * @param $extions
     * @return object|array|void|mixed
     */
    public function downLoadFile($url, $savePath, $ext = '')
    {
        $savePath = trim($savePath, '/');
        $pathRes = $this->createSavePath($savePath);
        if ($pathRes['status'] != STATUS_CODE_SUCCESS) {
            return funReturn(STATUS_CODE_INSIDEERROR, '创建失败');
        }
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_REFERER, '');
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_HEADER, true); // 输出HTTP头 true
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);//SSL证书认证
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($curl, CURLOPT_TIMEOUT, 10);
        $response = curl_exec($curl);
        //分离header与body
        $header = '';
        $body = '';
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); //头信息size
        curl_close($curl);
        if ($httpCode != 200) {
            return funReturn(STATUS_CODE_INSIDEERROR, '下载失败');
        }
        $header = substr($response, 0, $headerSize);
        $body = substr($response, $headerSize);
        if (!$ext) {
            $urlInfo = parse_url($url);
            if (isset($urlInfo['path'][0])) {
                $pathInfo = pathinfo($urlInfo['path']);
                if (isset($pathInfo['extension'][0])) {
                    $ext = $pathInfo['extension'];
                }
            }
        }
        if (!$ext) {
            return funReturn(STATUS_CODE_INSIDEERROR, '未获取到文件类型');
        }
        $fileName = $this->createFileName($pathRes['data']) . '.' . $ext;
        if (file_put_contents($pathRes['data'] . '/' . $fileName, $body) === false) {
            return funReturn(STATUS_CODE_INSIDEERROR, '下载失败');
        }
        return funReturn(STATUS_CODE_SUCCESS, 'success', ['file' => $savePath . '/' . $fileName]);
    }


    /**
     * 生成随机文件名
     * User:端木凌风
     * Date:2023-11-08 09:44
     * @param $path
     * @return object|array|void|mixed
     */
    public function createFileName($path)
    {
        $name = $path . '/' . time() . '/' . rand(0, 10000) . rand(0, 10000);
        return md5($name);
    }


    /**
     * 获取域名
     * User:端木凌风
     * Date:2023-11-08 09:28
     * @return object|array|void|mixed
     */
    public function getDomain()
    {
        return self::$domain;
    }

    /**
     * 把存储路径转化为url地址
     * User:端木凌风
     * Date:2023-11-08 09:53
     * @param $savePath
     * @return object|array|void|mixed
     */
    public static function turnSavePathToUrl($savePath)
    {
        $savePath = trim($savePath, '/');
        $domain = self::$domain ? self::$domain : config('app.app_host');
        $domain = trim($domain, '/');
        return $savePath ? $domain . '/' . $savePath : '';
    }

    /**
     * 文件转成真实路径
     * User:端木凌风
     * Date:2023-11-08 09:05
     * @param $savePath
     * @return object|array|void|mixed
     */
    public function turnSavePathToRealPath($savePath)
    {
        $savePath = trim($savePath, '/');
        return public_path() . '/' . $savePath;
    }

    /**
     * 获取保存目录
     * User:端木凌风
     * Date:2023-11-08 09:54
     * @return object|array|void|mixed
     */
    public function getSaveDir()
    {
        return $this->saveDir;
    }

    /**
     * 创建文件名称
     * User:端木凌风
     * Date:2023-11-08 09:07
     * @param $savePath
     * @param $ext
     * @return object|array|void|mixed
     */
    public function createFileBaseName($savePath, $ext)
    {
        return md5($savePath . microtime(true) . mt_rand(1, 1000) . mt_rand(1, 1000) . mt_rand(1, 1000)) . '.' . $ext;
    }
}